/*
 * Copyright 1999-2004 Alibaba.com All right reserved. This software is the confidential and proprietary information of
 * Alibaba.com ("Confidential Information"). You shall not disclose such Confidential Information and shall use it only
 * in accordance with the terms of the license agreement you entered into with Alibaba.com.
 */
package action.expr;

import java.io.CharArrayReader;
import java.io.IOException;
import java.io.Reader;
import java.util.Iterator;
import java.util.NoSuchElementException;

import action.exception.ActionException;

/**
 * action���ʽ�ʷ�����
 * 
 * <pre>
 * EQ: =
 * ADD_EQ: +=
 * MINU_EQ: -=
 * MUL_EQ: *=
 * DIV_EQ: /=
 * KEY: [a-zA-Z$_][a-zA-Z0-9$_\.:]*
 * ADD: +
 * MINU: -
 * MUL: *
 * DIV: /
 * NUM: (-)?[0-9]+(\.[0-9]+)?
 * STR: '(\\[btnfr'\\]|[^\\'])*'
 * LR: (
 * RP: )
 * </pre>
 * 
 * @author pf-miles 2011-6-17 ����02:47:45
 */
public class ActionLexer implements Iterable<ActionToken> {

    private Reader reader;
    // 19 status total
    private static final int[] stats = new int[19];

    static {
        for (int i = 0; i < 19; i++)
            stats[i] = i;
    }
    // if eof token returned
    private boolean eofReturned = false;
    // char buffer, stores next n chars
    private char[] cb = new char[1024];
    // current char index, -1 when no char in buffer
    private int p = -1;
    // current valid char length in buffer
    private int validLength = 0;
    // null char
    private static final char nil = 0xffff;

    // the stateless, singleton iterator
    private final Iterator<ActionToken> iter = new Iterator<ActionToken>() {

        public boolean hasNext() {
            return p != -1 || !eofReturned;
        }

        public ActionToken next() {
            int status = stats[0];
            // currently matched string
            StringBuilder sb = new StringBuilder();
            // is final state (about to return a matched token)
            boolean fnl = false;
            ActionToken matchedToken = null;
            while (!fnl) {
                char c = currentChar();
                switch (status) {
                case 0:
                    if (isBlankChar(c)) {
                        // ignore any BLANK chars
                        p++;
                    } else if (')' == c) {
                        sb.append(cb[p++]);
                        status = 18;
                    } else if ('(' == c) {
                        sb.append(cb[p++]);
                        status = 17;
                    } else if (is0to9(c)) {
                        sb.append(cb[p++]);
                        status = 13;
                    } else if ('-' == c) {
                        sb.append(cb[p++]);
                        status = 12;
                    } else if ('=' == c) {
                        sb.append(cb[p++]);
                        status = 1;
                    } else if ('+' == c) {
                        sb.append(cb[p++]);
                        status = 2;
                    } else if ('*' == c) {
                        sb.append(cb[p++]);
                        status = 4;
                    } else if ('/' == c) {
                        sb.append(cb[p++]);
                        status = 6;
                    } else if (isa2zA2Z$_(c)) {
                        sb.append(cb[p++]);
                        status = 8;
                    } else if ('\'' == c) {
                        sb.append(cb[p++]);
                        status = 9;
                    } else if (nil == c) {
                        if (eofReturned) {
                            throw new NoSuchElementException("No more characters.");
                        } else {
                            eofReturned = true;
                            return new ActionToken(ActionTokenType.EOF, null);
                        }
                    } else {
                        throw new ActionException("Unexpected char: '" + c + "'");
                    }
                    break;
                case 1:
                    fnl = true;
                    matchedToken = new ActionToken(ActionTokenType.EQ, sb.toString());
                    break;
                case 2:
                    if ('=' == c) {
                        sb.append(cb[p++]);
                        status = 3;
                    } else {
                        fnl = true;
                        matchedToken = new ActionToken(ActionTokenType.ADD, sb.toString());
                    }
                    break;
                case 3:
                    fnl = true;
                    matchedToken = new ActionToken(ActionTokenType.ADD_EQ, sb.toString());
                    break;
                case 4:
                    if ('=' == c) {
                        sb.append(cb[p++]);
                        status = 5;
                    } else {
                        fnl = true;
                        matchedToken = new ActionToken(ActionTokenType.MUL, sb.toString());
                    }
                    break;
                case 5:
                    fnl = true;
                    matchedToken = new ActionToken(ActionTokenType.MUL_EQ, sb.toString());
                    break;
                case 6:
                    if ('=' == c) {
                        sb.append(cb[p++]);
                        status = 7;
                    } else {
                        fnl = true;
                        matchedToken = new ActionToken(ActionTokenType.DIV, sb.toString());
                    }
                    break;
                case 7:
                    fnl = true;
                    matchedToken = new ActionToken(ActionTokenType.DIV_EQ, sb.toString());
                    break;
                case 8:
                    if (isAlphaOrNum$_dotColon(c)) {
                        sb.append(cb[p++]);
                    } else {
                        fnl = true;
                        matchedToken = new ActionToken(ActionTokenType.KEY, sb.toString());
                    }
                    break;
                case 9:
                    if ('\\' == c) {
                        sb.append(cb[p++]);
                        status = 11;
                    } else if ('\'' == c) {
                        sb.append(cb[p++]);
                        status = 10;
                    } else if (!isCharShouldEscaped(c)) {
                        sb.append(cb[p++]);
                    } else {
                        throw new ActionException("The control char should be escaped: '\\" + (int) c + "'");
                    }
                    break;
                case 10:
                    fnl = true;
                    matchedToken = new ActionToken(ActionTokenType.STR, sb.toString());
                    break;
                case 11:
                    if (isEscapeChar(c)) {
                        sb.append(cb[p++]);
                        status = 9;
                    } else {
                        throw new ActionException("Unexpected escape char: '" + c + "'");
                    }
                    break;
                case 12:
                    if (is0to9(c)) {
                        sb.append(cb[p++]);
                        status = 13;
                    } else if ('=' == c) {
                        sb.append(cb[p++]);
                        status = 16;
                    } else {
                        fnl = true;
                        matchedToken = new ActionToken(ActionTokenType.MINU, sb.toString());
                    }
                    break;
                case 13:
                    if (is0to9(c)) {
                        sb.append(cb[p++]);
                    } else if ('.' == c) {
                        sb.append(cb[p++]);
                        status = 14;
                    } else {
                        fnl = true;
                        matchedToken = new ActionToken(ActionTokenType.NUM, sb.toString());
                    }
                    break;
                case 14:
                    if (is0to9(c)) {
                        sb.append(cb[p++]);
                        status = 15;
                    } else {
                        throw new ActionException("Unexpected char: '" + c + "'");
                    }
                    break;
                case 15:
                    if (is0to9(c)) {
                        sb.append(cb[p++]);
                    } else {
                        fnl = true;
                        matchedToken = new ActionToken(ActionTokenType.NUM, sb.toString());
                    }
                    break;
                case 16:
                    fnl = true;
                    matchedToken = new ActionToken(ActionTokenType.MINU_EQ, sb.toString());
                    break;
                case 17:
                    fnl = true;
                    matchedToken = new ActionToken(ActionTokenType.LP, sb.toString());
                    break;
                case 18:
                    fnl = true;
                    matchedToken = new ActionToken(ActionTokenType.RP, sb.toString());
                    break;
                }
            }
            return matchedToken;
        }

        private char currentChar() {
            if (p > validLength - 1 && validLength != -1 || validLength == 0) {
                readToBuffer();
            }
            if (validLength == -1) {
                return nil;
            } else {
                return cb[p];
            }
        }

        private void readToBuffer() {
            try {
                validLength = reader.read(cb);
                if (validLength != -1) {
                    p = 0;
                } else {
                    p = -1;
                }
            } catch (IOException e) {
                throw new ActionException(e);
            }
        }

        private boolean isCharShouldEscaped(char c) {
            return '\b' == c || '\t' == c || '\n' == c || '\f' == c || '\r' == c;
        }

        private boolean isEscapeChar(char c) {
            return 'b' == c || 't' == c || 'n' == c || 'f' == c || 'r' == c || '\'' == c || '\\' == c;
        }

        private boolean isAlphaOrNum$_dotColon(char c) {
            return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || '$' == c
                    || '_' == c || '.' == c || ':' == c;
        }

        private boolean isa2zA2Z$_(char c) {
            return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || '$' == c || '_' == c;
        }

        private boolean is0to9(char c) {
            return c >= '0' && c <= '9';
        }

        private boolean isBlankChar(char c) {
            return c >= 0 && c <= 32 || c == 127;
        }

        public void remove() {
            throw new UnsupportedOperationException("Not supported.");
        }
    };

    /**
     * lexing upon specified string
     * 
     * @param code
     */
    public ActionLexer(String code) {
        this.reader = new CharArrayReader(code.toCharArray());
    }

    /**
     * lexing upon given reader
     * 
     * @param reader
     */
    public ActionLexer(Reader reader) {
        this.reader = reader;
    }

    public Iterator<ActionToken> iterator() {
        return this.iter;
    }
}
